#include <ne.h>
#include <system/GlutApplication.h>

int Width = 1280;
int Height = 720;
int ShadowSize = 2048;
float SkyTextureSize = 512.0f;
float SkyBoxSize = 1024.0f;

std::string skyBoxImageMap[6] = {"../image/skybox/evening_RT.jpg",
                                 "../image/skybox/evening_LF.jpg",
                                 "../image/skybox/evening_UP.jpg",
                                 "../image/skybox/evening_DN.jpg",
                                 "../image/skybox/evening_BK.jpg",
                                 "../image/skybox/evening_FR.jpg"};

int lineMode = 0;
int alphaMode = 0;
ne::Engine *pEngine = 0;
ne::Light *light = 0;
ne::Timer timer;
ne::Camera *camera = 0;
ne::Viewport *viewport = 0;
ne::GlutApplication *pApplication = 0;
ne::GLFrameBufferObject *pFrameBuffer = 0;
ne::GLRenderBufferObject *pRenderBuffer = 0;
ne::TexturePtr pTexture;
ne::TexturePtr pSkyTexture;
ne::GLSLProgramPtr pProgram;
ne::Vector2 mousePosition;
ne::Vector3 meshPos;
bool mouseLeftOn = false;
bool mouseRightOn = false;
bool keyCtrlOn = false;

int sampleLocation = 0;

void mouseFunc(int button, int state, int x, int y);
void motionFunc(int x, int y);
void loop(void);
void redraw(void);
void drawFPS(void);
void resize(int width, int height);
void init();

int main(int argc, char *argv[]) /* I - Command-line arguments */
{
    pApplication = new ne::GlutApplication();//(argc, argv);
    glutReshapeFunc(resize);
    glutDisplayFunc(loop);
    glutMouseFunc(mouseFunc);
    glutMotionFunc(motionFunc);

    pEngine = pApplication->engine;
    if (!pEngine || !pEngine->isReady())
    {
        delete pApplication;
        return 0;
    }

    init();

    glutMainLoop();

    delete pApplication;

    return 0;
}

void init()
{
    camera = new ne::Camera();
    viewport = new ne::Viewport(camera);
    camera->setPosition(ne::Vector3::UNIT_Z * 50.0f);

    glClearColor(0, 0, 0, 0);
    glClearDepth(1);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glDepthFunc(GL_LEQUAL);
    glEnable(GL_DEPTH_TEST);

    glCullFace(GL_BACK);
    glEnable(GL_CULL_FACE);

    glEnable(GL_COLOR_MATERIAL);
    glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);

    glShadeModel(GL_SMOOTH);
    glEnable(GL_POINT_SMOOTH);
    glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
    glEnable(GL_LINE_SMOOTH);
    glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
    glEnable(GL_POLYGON_SMOOTH);
    glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST);

    pFrameBuffer = new ne::GLFrameBufferObject();
    pFrameBuffer->bind();

    pRenderBuffer = new ne::GLRenderBufferObject();
    pRenderBuffer->setStorage(ne::TF_RGBA, ShadowSize, ShadowSize);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, pRenderBuffer->getSource());

    pTexture = pEngine->getTextureManager()->createTexture("ShadowMap", "ShadowMap", ne::TT_2D, ne::TF_DEPTH_COMPONENT, ne::PT_FLOAT, ShadowSize, ShadowSize);
    pTexture->setFilter(ne::TFF_LINEAR, ne::TFF_LINEAR);
    pTexture->createRenderTexture();
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, pTexture->getSource(), 0);

    std::cout << "Framebuffer " << pFrameBuffer->checkStatus() << std::endl;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    ne::ImagePtr pImage;
    pSkyTexture = pEngine->getTextureManager()->createTexture("SkyBox", "SkyBox", ne::TT_CUBE_MAP, ne::TF_RGB, ne::PT_UNSIGNED_BYTE, SkyTextureSize, SkyTextureSize);
    for (size_t i=0; i<6; ++i)
    {
        pImage = pEngine->getImageManager()->create("SkyBoxImage", skyBoxImageMap[i]);
        pImage->load();
        pSkyTexture->loadImageForCubeMap(ne::TextureCubeMapFace(ne::TCMF_POSITIVE_X + i), *pImage);
        pEngine->getImageManager()->removeByPtr(pImage);
    }
    pSkyTexture->createRenderTexture();

    ne::GLSLManager *pManager = pEngine->getGLSLManager();
    pProgram = pManager->create("ShadowMap", "../shader/shadow.glpg", true);
    ne::GLSLShaderPtr pVShader;
    pVShader = pProgram->createShaderFromFile("ShadowMapVertexShader", "../shader/shadow.glvs", ne::SHADER_TYPE_VERTEX);
    ne::GLSLShaderPtr pFShader;
    pFShader = pProgram->createShaderFromFile("ShadowMapFragmentShader", "../shader/shadowPCSS.glfs", ne::SHADER_TYPE_FRAGMENT);
    pProgram->attachShader(pVShader);
    pProgram->attachShader(pFShader);
    pProgram->link();

    camera->setFarClipDistance(2048.0f);

    // Set light
    light = new ne::Light();
    light->setType(ne::LT_POINT);
    light->setAmbient(ne::Color::GRAY);
    light->setAttenuationFactor(ne::LAT_LINEAR, 0.01f);
    light->setPosition(ne::Vector3(0.0f, 0.0f, 40.0f));

    /*
    ne::UIComponent *pUI = pEngine->getUIManager()->createUIComponent("UIButton", "Light Button");
    pUI->setPosition(ne::Vector3(0.0f, 0.0f, 0.0f));
    pUI->setSize(100.0f, 20.0f);
    ne::UIButton *pButton = dynamic_cast<ne::UIButton*>(pUI);
    pButton->setText("Light Off");
    pButton->setCallbackFunc(setLightOn);
    pButton->setColor(ne::Color(0.0f, 0.6f, 1.0f, 1.0f));
    pButton->setTextColor(ne::Color(1.0f, 1.0f, 1.0f));
    */
}

void mouseFunc(int button, int state, int x, int y)
{
    if (button == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
    {
        mousePosition.x = x;
        mousePosition.y = y;
        mouseLeftOn = true;
    }
    else if (button == GLUT_LEFT_BUTTON && state == GLUT_UP)
    {
        mouseLeftOn = false;
    }
    if (button == GLUT_RIGHT_BUTTON && state == GLUT_DOWN)
    {
        mousePosition.x = x;
        mousePosition.y = y;
        mouseRightOn = true;
    }
    else if (button == GLUT_RIGHT_BUTTON && state == GLUT_UP)
    {
        mouseRightOn = false;
    }
}

void motionFunc(int x, int y)
{
    if (mouseLeftOn)
    {
        if (keyCtrlOn)
        {
            ne::Ray outRay;
            viewport->getScreenToWorldRay(ne::real(x), ne::real(y), outRay);
            ne::Plane p(ne::Vector3::UNIT_Z, 0.0f);
            ne::real t = -1.0f;
            if (outRay.trace(p, t))
            {
                meshPos = outRay.getPoint(t);
            }
        }
        else
        {
            float dx = x - mousePosition.x;
            float dy = y - mousePosition.y;
            ne::Quaternion q(ne::Vector3::UNIT_Y, 0.01f * dx);
            light->setPosition(q * light->getPosition());
            light->rotate(q, ne::TS_WORLD);

            q.fromAxisAngle(light->getRight(), 0.01f * dy);
            light->setPosition(q * light->getPosition());
            light->rotate(q, ne::TS_WORLD);
            mousePosition.x = x;
            mousePosition.y = y;
        }
    }
    if (mouseRightOn)
    {
        float dx = x - mousePosition.x;
        float dy = y - mousePosition.y;
        camera->yaw(-0.01f * dx * ne::Math::PI / 36, ne::TS_WORLD);
        camera->pitch(-0.01f * dy * ne::Math::PI / 36);
        mousePosition.x = x;
        mousePosition.y = y;
    }
}

void loop(void)
{
    ne::Keyboard *pKeyboard = pEngine->getKeyboard();
    if (pKeyboard->isKeyDown(ne::KK_W))
    {
        camera->translate(0.1f * ne::Vector3::NEGATIVE_UNIT_Z, ne::TS_LOCAL);
    }
    if (pKeyboard->isKeyDown(ne::KK_S))
    {
        camera->translate(0.1f * ne::Vector3::UNIT_Z, ne::TS_LOCAL);
    }
    if (pKeyboard->isKeyDown(ne::KK_A))
    {
        camera->translate(0.1f * ne::Vector3::NEGATIVE_UNIT_X, ne::TS_LOCAL);
    }
    if (pKeyboard->isKeyDown(ne::KK_D))
    {
        camera->translate(0.1f * ne::Vector3::UNIT_X, ne::TS_LOCAL);
    }
    if (pKeyboard->isKeyDown(ne::KK_Z))
    {
        camera->translate(0.1f * ne::Vector3::NEGATIVE_UNIT_Y, ne::TS_PARENT);
    }
    if (pKeyboard->isKeyDown(ne::KK_Q))
    {
        camera->translate(0.1f * ne::Vector3::UNIT_Y, ne::TS_PARENT);
    }
    keyCtrlOn = pKeyboard->isKeyDown(ne::KK_C);
    if (pKeyboard->isKeyDown(ne::KK_ESC))
    {
        exit(0);
    }
    redraw();
}

void drawSky(void)
{
    glDisable(GL_DEPTH_TEST);
    glDepthMask(GL_FALSE);
    glDisable(GL_LIGHTING);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(pSkyTexture->getType(), pSkyTexture->getSource());
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
    glEnable(pSkyTexture->getType());

    glBegin(GL_QUADS);
        glTexCoord3f(-1.0f, 1.0f, 1.0f);
        glVertex3f(-SkyBoxSize, SkyBoxSize, SkyBoxSize);
        glTexCoord3f(-1.0f, -1.0f, 1.0f);
        glVertex3f(-SkyBoxSize, -SkyBoxSize, SkyBoxSize);
        glTexCoord3f(-1.0f, -1.0f, -1.0f);
        glVertex3f(-SkyBoxSize, -SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(-1.0f, 1.0f, -1.0f);
        glVertex3f(-SkyBoxSize, SkyBoxSize, -SkyBoxSize);

        glTexCoord3f(1.0f, 1.0f, 1.0f);
        glVertex3f(SkyBoxSize, SkyBoxSize, SkyBoxSize);
        glTexCoord3f(1.0f, 1.0f, -1.0f);
        glVertex3f(SkyBoxSize, SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(1.0f, -1.0f, -1.0f);
        glVertex3f(SkyBoxSize, -SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(1.0f, -1.0f, 1.0f);
        glVertex3f(SkyBoxSize, -SkyBoxSize, SkyBoxSize);

        glTexCoord3f(-1.0f, -1.0f, -1.0f);
        glVertex3f(-SkyBoxSize, -SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(-1.0f, -1.0f, 1.0f);
        glVertex3f(-SkyBoxSize, -SkyBoxSize, SkyBoxSize);
        glTexCoord3f(1.0f, -1.0f, 1.0f);
        glVertex3f(SkyBoxSize, -SkyBoxSize, SkyBoxSize);
        glTexCoord3f(1.0f, -1.0f, -1.0f);
        glVertex3f(SkyBoxSize, -SkyBoxSize, -SkyBoxSize);

        glTexCoord3f(-1.0f, 1.0f, -1.0f);
        glVertex3f(-SkyBoxSize, SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(1.0f, 1.0f, -1.0f);
        glVertex3f(SkyBoxSize, SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(1.0f, 1.0f, 1.0f);
        glVertex3f(SkyBoxSize, SkyBoxSize, SkyBoxSize);
        glTexCoord3f(-1.0f, 1.0f, 1.0f);
        glVertex3f(-SkyBoxSize, SkyBoxSize, SkyBoxSize);

        glTexCoord3f(1.0f, 1.0f, -1.0f);
        glVertex3f(SkyBoxSize, SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(-1.0f, 1.0f, -1.0f);
        glVertex3f(-SkyBoxSize, SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(-1.0f, -1.0f, -1.0f);
        glVertex3f(-SkyBoxSize, -SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(1.0f, -1.0f, -1.0f);
        glVertex3f(SkyBoxSize, -SkyBoxSize, -SkyBoxSize);

        glTexCoord3f(1.0f, 1.0f, 1.0f);
        glVertex3f(SkyBoxSize, SkyBoxSize, SkyBoxSize);
        glTexCoord3f(1.0f, -1.0f, 1.0f);
        glVertex3f(SkyBoxSize, -SkyBoxSize, SkyBoxSize);
        glTexCoord3f(-1.0f, -1.0f, 1.0f);
        glVertex3f(-SkyBoxSize, -SkyBoxSize, SkyBoxSize);
        glTexCoord3f(-1.0f, 1.0f, 1.0f);
        glVertex3f(-SkyBoxSize, SkyBoxSize, SkyBoxSize);
    glEnd();
    glDisable(pSkyTexture->getType());

    glEnable(GL_DEPTH_TEST);
    glDepthMask(GL_TRUE);
    glEnable(GL_LIGHTING);
}

void drawMesh(void)
{
    glPushMatrix();

    glTranslatef(meshPos.x, meshPos.y, meshPos.z);

    glColor4f(0.0f, 1.0f, 0.0f, 1.0f);
    glutSolidCube(8);

    glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
    glutSolidTorus(1, 6, 72, 36);

    glPushMatrix();
        glColor4f(1.0f, 1.0f, 0.0f, 1.0f);
        glTranslatef(6.0f, 0.0f, 6.0f);
        glutSolidSphere(2.0f, 20, 20);
    glPopMatrix();

    glPushMatrix();
        glColor4f(0.0f, 0.6f, 1.0f, 1.0f);
        glTranslatef(-6.0f, 0.0f, 6.0f);
        glutSolidSphere(2.0f, 20, 20);
    glPopMatrix();

    glPushMatrix();
        glColor4f(1.0f, 0.5f, 0.0f, 1.0f);
        glTranslatef(-6.0f, 0.0f, -6.0f);
        glutSolidSphere(2.0f, 20, 20);
    glPopMatrix();

    glPushMatrix();
        glColor4f(0.6f, 0.0f, 1.0f, 1.0f);
        glTranslatef(6.0f, 0.0f, -6.0f);
        glutSolidSphere(2.0f, 20, 20);
    glPopMatrix();

    glPopMatrix();

    glColor4f(0.8f, 0.8f, 0.8f, 1.0f);
    glNormal3f(0.0f, 1.0f, 0.0f);
    glBegin(GL_QUADS);
        glTexCoord2f(0.0f, 1.0f);
        glVertex3f(-100, -10, 100);
        glTexCoord2f(0.0f, 0.0f);
        glVertex3f(100, -10, 100);
        glTexCoord2f(1.0f, 0.0f);
        glVertex3f(100, -10, -100);
        glTexCoord2f(1.0f, 1.0f);
        glVertex3f(-100, -10, -100);
    glEnd();
}

void redraw(void)
{
    // First pass Make ShadowMap
    glEnable(GL_FRAMEBUFFER);
    pFrameBuffer->bind();

    int w = Width;
    int h = Height;
    resize(ShadowSize, ShadowSize);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glMatrixMode(GL_MODELVIEW);
    ne::Matrix4 lightMatrix = light->getViewMatrix();
    glLoadMatrixf(lightMatrix.ptr());
    lightMatrix = lightMatrix * camera->getProjectionMatrix();

    glPushMatrix();
        drawMesh();
    glPopMatrix();

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glDisable(GL_FRAMEBUFFER);

    // Second pass Render Object
    glEnable(GL_LIGHTING);
    glEnable(GL_COLOR_SUM);

    resize(w, h);

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glMatrixMode(GL_MODELVIEW);
    const ne::Matrix4 &viewMatrix = camera->getViewMatrix();
    ne::Matrix4 modelViewProjectionInverseMatrix = (viewMatrix * camera->getProjectionMatrix()).inverse();
    glLoadMatrixf(viewMatrix.ptr());

    glPushMatrix();
        ne::Vector3 pos = camera->getPosition();
        glTranslatef(pos.x, pos.y, pos.z);
        drawSky();
    glPopMatrix();

    glPushMatrix();
        light->useLight();
        glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SEPARATE_SPECULAR_COLOR);
        glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ne::Color::WHITE.ptr());

        pProgram->useProgram();
        pProgram->setUniform("lightViewProjectionMatrix", lightMatrix);
        pProgram->setUniform("modelViewProjectionInverseMatrix", modelViewProjectionInverseMatrix);
        pProgram->setUniform("lightPos", light->getPosition());
        pProgram->setUniform("cameraPos", camera->getPosition());
        pProgram->setUniform("shadowMap", 0);
        pProgram->setUniform("shadowColorMap", 1);
        pProgram->setUniform("shadowSize", ShadowSize);
        pProgram->validate();

        glBindTexture(pTexture->getType(), pTexture->getSource());
        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
        pTexture->createRenderTexture();
        glEnable(pTexture->getType());

        glActiveTexture(GL_TEXTURE1);
        glBindTexture(pSkyTexture->getType(), pSkyTexture->getSource());
        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
        glEnable(pSkyTexture->getType());

        drawMesh();

        glDisable(pTexture->getType());
        glDisable(pSkyTexture->getType());
        glActiveTexture(GL_TEXTURE0);
        pProgram->useProgram(false);

        // Draw light
        glColor4f(1.0f, 1.0f, 0.0f, 1.0f);
        glPushMatrix();
            ne::Vector3 lpos = light->getPosition();
            glTranslatef(lpos.x, lpos.y, lpos.z);
            glutSolidSphere(1.0f, 36, 18);
        glPopMatrix();

        // Draw Bezier line
        glDisable(GL_CULL_FACE);
        glColor4f(0.0f, 0.6f, 1.0f, 1.0f);
        glPushMatrix();
            ne::Vector3 vS(-50.0f, 0.0f, 0.0f);
            ne::Vector3 vA(-20.0f, 20.0f, 40.0f);
            ne::Vector3 vB(20.0f, -20.0f, -40.0f);
            ne::Vector3 vD(50.0f, 0.0f, 0.0f);
            glBegin(GL_TRIANGLE_STRIP);
            for (ne::real u=0.0f; u<=1.0f; u+=0.02f)
            {
                ne::Vector3 p = ne::Vector3::Bezier(vS, vA, vB, vD, u);
                glVertex3fv(p.ptr());
                glVertex3fv((p + ne::Vector3(0.0f, 8.0f, 0.0f)).ptr());
            }
            glEnd();
        glPopMatrix();
        glEnable(GL_CULL_FACE);

    glPopMatrix();

    glDisable(GL_COLOR_SUM);

    glDisable(GL_LIGHTING);

    // Display shadow map
    glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();
	glOrtho(0.0f, viewport->getWidth(),
            viewport->getHeight(), 0.0f,
            1.0f, -1.0f);

	glMatrixMode(GL_MODELVIEW);
	glPushMatrix();
	glLoadIdentity();
    glBindTexture(pTexture->getType(), pTexture->getSource());
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
    glEnable(pTexture->getType());
    glColor4f(1.0f, 1.0f, 1.0f, 0.5f);
    glBegin(GL_QUADS);
    {
        glTexCoord2f(0.0f, 1.0f);
        glVertex3f(1024, 0, 0);
        glTexCoord2f(0.0f, 0.0f);
        glVertex3f(1024, 256, 0);
        glTexCoord2f(1.0f, 0.0f);
        glVertex3f(1280, 256, 0);
        glTexCoord2f(1.0f, 1.0f);
        glVertex3f(1280, 0, 0);
    }
    glEnd();
    glDisable(pTexture->getType());
	//reset matrices
	glMatrixMode(GL_PROJECTION);
	glPopMatrix();
	glMatrixMode(GL_MODELVIEW);
	glPopMatrix();

    drawFPS();

    glutSwapBuffers();
    glutPostRedisplay();
}

void resize(int width, int height)
{
    /* Save the new width and height */
    Width  = width;
    Height = height;

    /* Reset the viewport->.. */
    glViewport(0, 0, width, height);
    viewport->setPosition(0, 0);
    viewport->resize(width, height);

    glMatrixMode(GL_PROJECTION);
    const ne::Matrix4 &projectionMatrix = camera->getProjectionMatrix();
    glLoadMatrixf(projectionMatrix.ptr());
    //gluPerspective(45, double(width)/double(height), 1.0, 1000.0);
    glMatrixMode(GL_MODELVIEW);
}

void drawFPS()
{
    glClear(GL_DEPTH_BUFFER_BIT);

    //Set matrices for ortho
	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();
	glOrtho(0.0f, viewport->getWidth(),
            0.0f, viewport->getHeight(),
            1.0f, -1.0f);

	glMatrixMode(GL_MODELVIEW);
	glPushMatrix();
	glLoadIdentity();

	//Print text
    ne::FontPtr font = pEngine->getFont();
    ne::Vector3 pos(Width - 50.0f, 10.0f, 0.0f);
    float w = font->getFontWidth();
    float h = font->getFontHeight();

    char fpsString[32];
    ne::uint32_t interval = timer.getIntervalMilliseconds();
	sprintf(fpsString, "%.2f", 1000.0f / interval);

    font->renderText2D(fpsString, pos, 6.0f*w, h, ne::Color(0.8f, 1.0f, 0.0f, 1.0f));

    //pEngine->getUIManager()->render();

    //reset matrices
	glMatrixMode(GL_PROJECTION);
	glPopMatrix();
	glMatrixMode(GL_MODELVIEW);
	glPopMatrix();
}

